(function($, undefined) {
    'use strict';
    var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
    var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
    var IDBCursor = window.IDBCursor || window.webkitIDBCursor || {};
    if (typeof IDBCursor.PREV === "undefined") {
        IDBCursor.PREV = "prev";
    }
    if (typeof IDBCursor.NEXT === "undefined") {
        IDBCursor.NEXT = "next";
    }

    /**
     * Best to use the constant IDBTransaction since older version support numeric types while the latest spec
     * supports strings
     */
    var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;

    function getDefaultTransaction(mode) {
        var result = null;
        switch (mode) {
            case 0:
            case 1:
            case "readwrite":
            case "readonly":
                result = mode;
                break;
            default:
                result = IDBTransaction.READ_WRITE || "readwrite";
        }
        return result;
    }

    $.extend({
        /**
         * The IndexedDB object used to open databases
         * @param {Object} dbName - name of the database
         * @param {Object} config - version, onupgradeneeded, onversionchange, schema
         */
        "indexedDB": function(dbName, config) {
            if (config) {
                // Parse the config argument
                if (typeof config === "number") config = {
                    "version": config
                };

                var version = config.version;
                if (config.schema && !version) {
                    var max = -1;
                    for (var key in config.schema) {
                        max = max > key ? max : key;
                    }
                    version = config.version || max;
                }
            }


            var wrap = {
                "request": function(req, args) {
                    return $.Deferred(function(dfd) {
                        try {
                            var idbRequest = typeof req === "function" ? req(args) : req;
                            idbRequest.onsuccess = function(e) {

                                dfd.resolveWith(idbRequest, [idbRequest.result, e]);
                            };
                            idbRequest.onerror = function(e) {

                                dfd.rejectWith(idbRequest, [idbRequest.error, e]);
                            };
                            if (typeof idbRequest.onblocked !== "undefined" && idbRequest.onblocked === null) {
                                idbRequest.onblocked = function(e) {

                                    var res;
                                    try {
                                        res = idbRequest.result;
                                    } catch (e) {
                                        res = null; // Required for Older Chrome versions, accessing result causes error
                                    }
                                    dfd.notifyWith(idbRequest, [res, e]);
                                };
                            }
                            if (typeof idbRequest.onupgradeneeded !== "undefined" && idbRequest.onupgradeneeded === null) {
                                idbRequest.onupgradeneeded = function(e) {

                                    dfd.notifyWith(idbRequest, [idbRequest.result, e]);
                                };
                            }
                        } catch (e) {
                            e.name = "exception";
                            dfd.rejectWith(idbRequest, ["exception", e]);
                        }
                    });
                },
                // Wraps the IDBTransaction to return promises, and other dependent methods
                "transaction": function(idbTransaction) {
                    return {
                        "objectStore": function(storeName) {
                            try {
                                return wrap.objectStore(idbTransaction.objectStore(storeName));
                            } catch (e) {
                                idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
                                return wrap.objectStore(null);
                            }
                        },
                        "createObjectStore": function(storeName, storeParams) {
                            try {
                                return wrap.objectStore(idbTransaction.db.createObjectStore(storeName, storeParams));
                            } catch (e) {
                                idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
                            }
                        },
                        "deleteObjectStore": function(storeName) {
                            try {
                                idbTransaction.db.deleteObjectStore(storeName);
                            } catch (e) {
                                idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
                            }
                        },
                        "abort": function() {
                            idbTransaction.abort();
                        }
                    };
                },
                "objectStore": function(idbObjectStore) {
                    var result = {};
                    // Define CRUD operations
                    var crudOps = ["add", "put", "get", "delete", "clear", "count"];
                    for (var i = 0; i < crudOps.length; i++) {
                        result[crudOps[i]] = (function(op) {
                            return function() {
                                return wrap.request(function(args) {
                                    return idbObjectStore[op].apply(idbObjectStore, args);
                                }, arguments);
                            };
                        })(crudOps[i]);
                    }

                    result.each = function(callback, range, direction) {
                        return wrap.cursor(function() {
                            if (direction) {
                                return idbObjectStore.openCursor(wrap.range(range), direction);
                            } else {
                                return idbObjectStore.openCursor(wrap.range(range));
                            }
                        }, callback);
                    };

                    result.index = function(name) {
                        return wrap.index(function() {
                            return idbObjectStore.index(name);
                        });
                    };

                    result.createIndex = function(prop, options, indexName) {
                        if (arguments.length === 2 && typeof options === "string") {
                            indexName = arguments[1];
                            options = null;
                        }
                        if (!indexName) {
                            indexName = prop;
                        }
                        return wrap.index(function() {
                            return idbObjectStore.createIndex(indexName, prop, options);
                        });
                    };

                    result.deleteIndex = function(indexName) {
                        return idbObjectStore.deleteIndex(indexName);
                    };

                    return result;
                },

                "range": function(r) {
                    if ($.isArray(r)) {
                        if (r.length === 1) {
                            return IDBKeyRange.only(r[0]);
                        } else {
                            return IDBKeyRange.bound(r[0], r[1], (typeof r[2] === 'undefined') ? false : r[2], (typeof r[3] === 'undefined') ? false : r[3]);
                        }
                    } else if (typeof r === "undefined") {
                        return null;
                    } else {
                        return r;
                    }
                },

                "cursor": function(idbCursor, callback) {
                    return $.Deferred(function(dfd) {
                        try {

                            var cursorReq = typeof idbCursor === "function" ? idbCursor() : idbCursor;
                            cursorReq.onsuccess = function(e) {

                                if (!cursorReq.result) {
                                    dfd.resolveWith(cursorReq, [null, e]);
                                    return;
                                }
                                var elem = {
                                    // Delete, update do not move
                                    "delete": function() {
                                        return wrap.request(function() {
                                            return cursorReq.result["delete"]();
                                        });
                                    },
                                    "update": function(data) {
                                        return wrap.request(function() {
                                            return cursorReq.result["update"](data);
                                        });
                                    },
                                    "next": function(key) {
                                        this.data = key;
                                    },
                                    "key": cursorReq.result.key,
                                    "value": cursorReq.result.value
                                };

                                dfd.notifyWith(cursorReq, [elem, e]);
                                var result = callback.apply(cursorReq, [elem]);

                                try {
                                    if (result === false) {
                                        dfd.resolveWith(cursorReq, [null, e]);
                                    } else if (typeof result === "number") {
                                        cursorReq.result["advance"].apply(cursorReq.result, [result]);
                                    } else {
                                        if (elem.data) cursorReq.result["continue"].apply(cursorReq.result, [elem.data]);
                                        else cursorReq.result["continue"]();
                                    }
                                } catch (e) {

                                    dfd.rejectWith(cursorReq, [cursorReq.result, e]);
                                }
                            };
                            cursorReq.onerror = function(e) {

                                dfd.rejectWith(cursorReq, [cursorReq.result, e]);
                            };
                        } catch (e) {

                            e.type = "exception";
                            dfd.rejectWith(cursorReq, [null, e]);
                        }
                    });
                },

                "index": function(index) {
                    try {
                        var idbIndex = (typeof index === "function" ? index() : index);
                    } catch (e) {
                        idbIndex = null;
                    }

                    return {
                        "each": function(callback, range, direction) {
                            return wrap.cursor(function() {
                                if (direction) {
                                    return idbIndex.openCursor(wrap.range(range), direction);
                                } else {
                                    return idbIndex.openCursor(wrap.range(range));
                                }

                            }, callback);
                        },
                        "eachKey": function(callback, range, direction) {
                            return wrap.cursor(function() {
                                if (direction) {
                                    return idbIndex.openKeyCursor(wrap.range(range), direction);
                                } else {
                                    return idbIndex.openKeyCursor(wrap.range(range));
                                }
                            }, callback);
                        },
                        "get": function(key) {
                            if (typeof idbIndex.get === "function") {
                                return wrap.request(idbIndex.get(key));
                            } else {
                                return idbIndex.openCursor(wrap.range(key));
                            }
                        },
                        "count": function() {
                            if (typeof idbIndex.count === "function") {
                                return wrap.request(idbIndex.count());
                            } else {
                                throw "Count not implemented for cursors";
                            }
                        },
                        "getKey": function(key) {
                            if (typeof idbIndex.getKey === "function") {
                                return wrap.request(idbIndex.getKey(key));
                            } else {
                                return idbIndex.openKeyCursor(wrap.range(key));
                            }
                        }
                    };
                }
            };


            // Start with opening the database
            var dbPromise = wrap.request(function() {

                return version ? indexedDB.open(dbName, parseInt(version)) : indexedDB.open(dbName);
            });
            dbPromise.then(function(db, e) {

                db.onversionchange = function() {
                    // Try to automatically close the database if there is a version change request
                    if (!(config && config.onversionchange && config.onversionchange() !== false)) {
                        db.close();
                    }
                };
            }, function(error, e) {

                // Nothing much to do if an error occurs
            }, function(db, e) {
                if (e && e.type === "upgradeneeded") {
                    if (config && config.schema) {
                        // Assuming that version is always an integer

                        for (var i = e.oldVersion + 1; i <= e.newVersion; i++) {
                            typeof config.schema[i] === "function" && config.schema[i].call(this, wrap.transaction(this.transaction));
                        }
                    }
                    if (config && typeof config.upgrade === "function") {
                        config.upgrade.call(this, wrap.transaction(this.transaction));
                    }
                }
            });

            return $.extend(dbPromise, {
                "cmp": function(key1, key2) {
                    return indexedDB.cmp(key1, key2);
                },
                "deleteDatabase": function() {
                    // Kinda looks ugly coz DB is opened before it needs to be deleted.
                    // Blame it on the API
                    return $.Deferred(function(dfd) {
                        dbPromise.then(function(db, e) {
                            db.close();
                            wrap.request(function() {
                                return indexedDB.deleteDatabase(dbName);
                            }).then(function(result, e) {
                                dfd.resolveWith(this, [result, e]);
                            }, function(error, e) {
                                dfd.rejectWith(this, [error, e]);
                            }, function(db, e) {
                                dfd.notifyWith(this, [db, e]);
                            });
                        }, function(error, e) {
                            dfd.rejectWith(this, [error, e]);
                        }, function(db, e) {
                            dfd.notifyWith(this, [db, e]);
                        });
                    });
                },
                "transaction": function(storeNames, mode) {
                    !$.isArray(storeNames) && (storeNames = [storeNames]);
                    mode = getDefaultTransaction(mode);
                    return $.Deferred(function(dfd) {
                        dbPromise.then(function(db, e) {
                            var idbTransaction;
                            try {

                                idbTransaction = db.transaction(storeNames, mode);

                                idbTransaction.onabort = idbTransaction.onerror = function(e) {
                                    dfd.rejectWith(idbTransaction, [e]);
                                };
                                idbTransaction.oncomplete = function(e) {
                                    dfd.resolveWith(idbTransaction, [e]);
                                };
                            } catch (e) {

                                e.type = "exception";
                                dfd.rejectWith(this, [e]);
                                return;
                            }
                            try {
                                dfd.notifyWith(idbTransaction, [wrap.transaction(idbTransaction)]);
                            } catch (e) {
                                e.type = "exception";
                                dfd.rejectWith(this, [e]);
                            }
                        }, function(err, e) {
                            dfd.rejectWith(this, [e, err]);
                        }, function(res, e) {

                            //dfd.notifyWith(this, ["", e]);
                        });

                    });
                },
                "objectStore": function(storeName, mode) {
                    var me = this,
                        result = {};

                    function op(callback) {
                        return $.Deferred(function(dfd) {
                            function onTransactionProgress(trans, callback) {
                                try {

                                    callback(trans.objectStore(storeName)).then(function(result, e) {
                                        dfd.resolveWith(this, [result, e]);
                                    }, function(err, e) {
                                        dfd.rejectWith(this, [err, e]);
                                    });
                                } catch (e) {

                                    e.name = "exception";
                                    dfd.rejectWith(trans, [e, e]);
                                }
                            }
                            me.transaction(storeName, getDefaultTransaction(mode)).then(function() {

                                // Nothing to do when transaction is complete
                            }, function(err, e) {
                                // If transaction fails, CrudOp fails
                                if (err.code === err.NOT_FOUND_ERR && (mode === true || typeof mode === "object")) {

                                    var db = this.result;
                                    db.close();
                                    dbPromise = wrap.request(function() {

                                        return indexedDB.open(dbName, (parseInt(db.version, 10) || 1) + 1);
                                    });
                                    dbPromise.then(function(db, e) {

                                        db.onversionchange = function() {
                                            // Try to automatically close the database if there is a version change request
                                            if (!(config && config.onversionchange && config.onversionchange() !== false)) {
                                                db.close();
                                            }
                                        };
                                        me.transaction(storeName, getDefaultTransaction(mode)).then(function() {

                                            // Nothing much to do
                                        }, function(err, e) {
                                            dfd.rejectWith(this, [err, e]);
                                        }, function(trans, e) {

                                            onTransactionProgress(trans, callback);
                                        });
                                    }, function(err, e) {
                                        dfd.rejectWith(this, [err, e]);
                                    }, function(db, e) {
                                        if (e.type === "upgradeneeded") {
                                            try {

                                                db.createObjectStore(storeName, mode === true ? {
                                                    "autoIncrement": true
                                                } : mode);

                                            } catch (ex) {

                                                dfd.rejectWith(this, [ex, e]);
                                            }
                                        }
                                    });
                                } else {
                                    dfd.rejectWith(this, [err, e]);
                                }
                            }, function(trans) {

                                onTransactionProgress(trans, callback);
                            });
                        });
                    }

                    function crudOp(opName, args) {
                        return op(function(wrappedObjectStore) {
                            return wrappedObjectStore[opName].apply(wrappedObjectStore, args);
                        });
                    }

                    function indexOp(opName, indexName, args) {
                        return op(function(wrappedObjectStore) {
                            var index = wrappedObjectStore.index(indexName);
                            return index[opName].apply(index[opName], args);
                        });
                    }

                    var crud = ["add", "delete", "get", "put", "clear", "count", "each"];
                    for (var i = 0; i < crud.length; i++) {
                        result[crud[i]] = (function(op) {
                            return function() {
                                return crudOp(op, arguments);
                            };
                        })(crud[i]);
                    }

                    result.index = function(indexName) {
                        return {
                            "each": function(callback, range, direction) {
                                return indexOp("each", indexName, [callback, range, direction]);
                            },
                            "eachKey": function(callback, range, direction) {
                                return indexOp("eachKey", indexName, [callback, range, direction]);
                            },
                            "get": function(key) {
                                return indexOp("get", indexName, [key]);
                            },
                            "count": function() {
                                return indexOp("count", indexName, []);
                            },
                            "getKey": function(key) {
                                return indexOp("getKey", indexName, [key]);
                            }
                        };
                    };

                    return result;
                }
            });
        }
    });

    $.indexedDB.IDBCursor = IDBCursor;
    $.indexedDB.IDBTransaction = IDBTransaction;
    $.idb = $.indexedDB;
})(jQuery);